/***************************************************************************** * * Copyright (C) Zenoss, Inc. 2010-2011, all rights reserved. * * This content is made available according to terms specified in * License.zenoss under the directory where your Zenoss product is installed. * ****************************************************************************/ package org.zenoss.protobufs; import com.google.protobuf.ByteString; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.EnumDescriptor; import com.google.protobuf.Descriptors.EnumValueDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor.JavaType; import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.ExtensionRegistry.ExtensionInfo; import com.google.protobuf.Message; import com.google.protobuf.Message.Builder; import org.codehaus.jackson.JsonEncoding; import org.codehaus.jackson.JsonFactory; import org.codehaus.jackson.JsonGenerator; import org.codehaus.jackson.JsonParser; import org.codehaus.jackson.JsonToken; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; /** * Class which can serialize Google protobufs to JSON format. */ public class JsonFormat { private static final JsonFactory FACTORY = new JsonFactory(); private JsonFormat() { } /** * Encodes the Google protobuf object to JSON format. * * @param message * The protobuf message to serialize. * @param output * The output stream to store the serialized message. * @throws IOException * If an exception occurs. */ public static void writeTo(Message message, OutputStream output) throws IOException { JsonGenerator generator = null; try { generator = FACTORY.createJsonGenerator(output, JsonEncoding.UTF8); writeMessage(generator, message); } finally { if (generator != null) { generator.close(); } } } /** * Encodes the Google protobuf object to JSON format. * * @param message * THe protobuf message to serialize. * @param writer * The writer where the protobuf is stored. * @throws IOException * If an exception occurs. */ public static void writeTo(Message message, Writer writer) throws IOException { JsonGenerator generator = null; try { generator = FACTORY.createJsonGenerator(writer); writeMessage(generator, message); } finally { if (generator != null) { generator.close(); } } } /** * Encodes the Google protobuf object to JSON format. * * @param message * THe protobuf message to serialize. * @return A JSON string of the encoded message. * @throws IOException * If an exception occurs. */ public static String writeAsString(Message message) throws IOException { JsonGenerator generator = null; StringWriter sw = new StringWriter(); try { generator = FACTORY.createJsonGenerator(sw); writeMessage(generator, message); } finally { if (generator != null) { generator.close(); } } return sw.toString(); } /** * Creates a protobuf message from the JSON input stream and message * builder. * * @param is * JSON input stream. * @param builder * Message builder. * @return The parsed protobuf message. * @throws IOException * If an exception occurs reading a protobuf object from the * JSON representation. */ public static Message merge(InputStream is, Builder builder) throws IOException { return merge(is, builder, ExtensionRegistry.getEmptyRegistry()); } /** * Creates a protobuf message from the JSON input stream and message * builder. * * @param is * JSON input stream. * @param builder * Message builder. * @param registry * Extension registry. * @return The parsed protobuf message. * @throws IOException * If an exception occurs reading a protobuf object from the * JSON representation. */ public static Message merge(InputStream is, Builder builder, ExtensionRegistry registry) throws IOException { JsonParser jp = null; try { jp = FACTORY.createJsonParser(is); jp.nextToken(); return readMessage(jp, builder, registry); } finally { if (jp != null) { jp.close(); } } } /** * Creates a protobuf message from the JSON input stream and message * builder. * * @param reader * JSON reader. * @param builder * Message builder. * @return The parsed protobuf message. * @throws IOException * If an exception occurs reading a protobuf object from the * JSON representation. */ public static Message merge(Reader reader, Builder builder) throws IOException { return merge(reader, builder, ExtensionRegistry.getEmptyRegistry()); } /** * Creates a protobuf message from the JSON input stream and message * builder. * * @param reader * JSON reader. * @param builder * Message builder. * @param registry * Extension registry. * @return The parsed protobuf message. * @throws IOException * If an exception occurs reading a protobuf object from the * JSON representation. */ public static Message merge(Reader reader, Builder builder, ExtensionRegistry registry) throws IOException { JsonParser jp = null; try { jp = FACTORY.createJsonParser(reader); jp.nextToken(); return readMessage(jp, builder, registry); } finally { if (jp != null) { jp.close(); } } } /** * Creates a protobuf message from the JSON input stream and message * builder. * * @param json * JSON string. * @param builder * Message builder. * @return The parsed protobuf message. * @throws IOException * If an exception occurs reading a protobuf object from the * JSON representation. */ public static Message merge(String json, Builder builder) throws IOException { return merge(json, builder, ExtensionRegistry.getEmptyRegistry()); } /** * Creates a protobuf message from the JSON input stream and message * builder. * * @param json * JSON string. * @param builder * Message builder. * @param registry * Extension registry. * @return The parsed protobuf message. * @throws IOException * If an exception occurs reading a protobuf object from the * JSON representation. */ public static Message merge(String json, Builder builder, ExtensionRegistry registry) throws IOException { JsonParser jp = null; try { jp = FACTORY.createJsonParser(new StringReader(json)); jp.nextToken(); return readMessage(jp, builder, registry); } finally { if (jp != null) { jp.close(); } } } private static Message readMessage(JsonParser jp, Builder builder, ExtensionRegistry registry) throws IOException { JsonToken tok = jp.getCurrentToken(); Descriptor descriptor = builder.getDescriptorForType(); if (tok != JsonToken.START_OBJECT) { throw new IOException("Expected START_OBJECT, found: " + tok); } while ((tok = jp.nextToken()) != JsonToken.END_OBJECT) { if (tok != JsonToken.FIELD_NAME) { throw new IOException("Expected FIELD_NAME, found: " + tok); } final String fieldName = jp.getCurrentName(); FieldDescriptor fieldDesc = descriptor.findFieldByName(fieldName); if (fieldDesc == null) { ExtensionInfo extensionInfo = registry.findExtensionByName(fieldName); if (extensionInfo != null) { fieldDesc = extensionInfo.descriptor; } } if (fieldDesc != null) { /* Advance to value token */ tok = jp.nextToken(); if (fieldDesc.isRepeated()) { if (tok != JsonToken.START_ARRAY) { throw new IOException("Expected START_ARRAY, found: " + tok); } builder.setField(fieldDesc, readRepeatable(jp, builder, registry, fieldDesc)); } else { builder.setField(fieldDesc, readValue(jp, builder, registry, fieldDesc)); } } else { /* Skip unknown field type */ tok = jp.nextToken(); if (tok == JsonToken.START_ARRAY || tok == JsonToken.START_OBJECT) { jp.skipChildren(); } } } return builder.build(); } private static List<Object> readRepeatable(JsonParser jp, Builder builder, ExtensionRegistry registry, FieldDescriptor desc) throws IOException { List<Object> l = new ArrayList<Object>(); while (jp.nextToken() != JsonToken.END_ARRAY) { l.add(readValue(jp, builder, registry, desc)); } return l; } private static Object readValue(JsonParser jp, Builder builder, ExtensionRegistry registry, FieldDescriptor desc) throws IOException { final Object val; switch (desc.getJavaType()) { case BOOLEAN: val = jp.getBooleanValue(); break; case BYTE_STRING: val = ByteString.copyFrom(jp.getBinaryValue()); break; case DOUBLE: val = jp.getDoubleValue(); break; case ENUM: EnumDescriptor enumDesc = desc.getEnumType(); val = enumDesc.findValueByNumber(jp.getIntValue()); break; case FLOAT: val = jp.getFloatValue(); break; case INT: val = jp.getIntValue(); break; case LONG: val = jp.getLongValue(); break; case MESSAGE: val = readMessage(jp, builder.newBuilderForField(desc), registry); break; case STRING: val = jp.getText(); break; default: throw new IllegalStateException("Unsupported java type: " + desc.getType()); } return val; } /** * Writes all of the specified messages in delimited format. * * @param messages * Messages to write. * @param output * Output stream where messages are written. * @throws IOException * If an exception occurs. */ public static void writeAllDelimitedTo( Collection<? extends Message> messages, OutputStream output) throws IOException { JsonFormatDelimitedEncoder encoder = null; try { encoder = new JsonFormatDelimitedEncoder(output); for (Message message : messages) { encoder.writeDelimitedTo(message); } } finally { if (encoder != null) { encoder.close(); } } } /** * Writes all of the specified messages in delimited format. * * @param messages * Messages to write. * @param writer * Writer where messages are written. * @throws IOException * If an exception occurs. */ public static void writeAllDelimitedTo( Collection<? extends Message> messages, Writer writer) throws IOException { JsonFormatDelimitedEncoder encoder = null; try { encoder = new JsonFormatDelimitedEncoder(writer); for (Message message : messages) { encoder.writeDelimitedTo(message); } } finally { if (encoder != null) { encoder.close(); } } } /** * Writes all of the specified messages in delimited format. * * @param messages * Messages to write. * @return A JSON encoded string. * @throws IOException * If an exception occurs. */ public static String writeAllDelimitedAsString( Collection<? extends Message> messages) throws IOException { JsonFormatDelimitedEncoder encoder = null; StringWriter sw = new StringWriter(); try { encoder = new JsonFormatDelimitedEncoder(sw); for (Message message : messages) { encoder.writeDelimitedTo(message); } } finally { if (encoder != null) { encoder.close(); } } return sw.toString(); } /** * Reads all messages from a delimited stream into a list. * * @param <T> * The type of message to read. * @param is * The input stream from which to read. * @param msg * The message type (used to create a builder) to read. * @return All delimited messages read from the stream. * @throws IOException * If an error occurs reading from the stream. */ public static <T extends Message> List<T> mergeAllDelimitedFrom( InputStream is, T msg) throws IOException { return mergeAllDelimitedFrom(is, msg, ExtensionRegistry.getEmptyRegistry()); } /** * Reads all messages from a delimited stream into a list. * * @param <T> * The type of message to read. * @param is * The input stream from which to read. * @param msg * The message type (used to create a builder) to read. * @param registry * The extension registry. * @return All delimited messages read from the stream. * @throws IOException * If an error occurs reading from the stream. */ @SuppressWarnings({"unchecked", "UnusedParameters"}) public static <T extends Message> List<T> mergeAllDelimitedFrom( InputStream is, T msg, ExtensionRegistry registry) throws IOException { JsonFormatDelimitedDecoder decoder = null; try { decoder = new JsonFormatDelimitedDecoder(is, registry); List<T> messages = new ArrayList<T>(); Message decoded; while ((decoded = decoder.mergeDelimitedFrom(msg)) != null) { messages.add((T) decoded); } return messages; } finally { if (decoder != null) { decoder.close(); } } } /** * Reads all messages from a delimited stream into a list. * * @param <T> * The type of message to read. * @param reader * The reader from which to read. * @param msg * The message type (used to create a builder) to read. * @return All delimited messages read from the stream. * @throws IOException * If an error occurs reading from the stream. */ public static <T extends Message> List<T> mergeAllDelimitedFrom( Reader reader, T msg) throws IOException { return mergeAllDelimitedFrom(reader, msg, ExtensionRegistry.getEmptyRegistry()); } /** * Reads all messages from a delimited stream into a list. * * @param <T> * The type of message to read. * @param reader * The reader from which to read. * @param msg * The message type (used to create a builder) to read. * @param registry * The extension registry. * @return All delimited messages read from the stream. * @throws IOException * If an error occurs reading from the stream. */ @SuppressWarnings({"unchecked", "UnusedParameters"}) public static <T extends Message> List<T> mergeAllDelimitedFrom( Reader reader, T msg, ExtensionRegistry registry) throws IOException { JsonFormatDelimitedDecoder decoder = null; try { decoder = new JsonFormatDelimitedDecoder(reader, registry); List<T> messages = new ArrayList<T>(); Message decoded; while ((decoded = decoder.mergeDelimitedFrom(msg)) != null) { messages.add((T) decoded); } return messages; } finally { if (decoder != null) { decoder.close(); } } } /** * Reads all messages from a delimited stream into a list. * * @param <T> * The type of message to read. * @param json * The encoded JSON string. * @param msg * The message type (used to create a builder) to read. * @return All delimited messages read from the stream. * @throws IOException * If an error occurs reading from the stream. */ public static <T extends Message> List<T> mergeAllDelimitedFrom( String json, T msg) throws IOException { return mergeAllDelimitedFrom(json, msg, ExtensionRegistry.getEmptyRegistry()); } /** * Reads all messages from a delimited stream into a list. * * @param <T> * The type of message to read. * @param json * The encoded JSON string. * @param msg * The message type (used to create a builder) to read. * @param registry * The extension registry. * @return All delimited messages read from the stream. * @throws IOException * If an error occurs reading from the stream. */ @SuppressWarnings({"unchecked", "UnusedParameters"}) public static <T extends Message> List<T> mergeAllDelimitedFrom( String json, T msg, ExtensionRegistry registry) throws IOException { JsonFormatDelimitedDecoder decoder = null; try { decoder = new JsonFormatDelimitedDecoder(new StringReader(json), registry); List<T> messages = new ArrayList<T>(); Message decoded; while ((decoded = decoder.mergeDelimitedFrom(msg)) != null) { messages.add((T) decoded); } return messages; } finally { if (decoder != null) { decoder.close(); } } } private static void writeMessage(JsonGenerator generator, Message message) throws IOException { generator.writeStartObject(); Map<FieldDescriptor, Object> fields = message.getAllFields(); for (Map.Entry<FieldDescriptor, Object> entry : fields.entrySet()) { FieldDescriptor key = entry.getKey(); Object val = entry.getValue(); final String name; if (key.isExtension()) { name = key.getFullName(); } else { name = key.getName(); } generator.writeFieldName(name); if (key.isRepeated()) { List<?> valList = (List<?>) val; generator.writeStartArray(); for (Object v : valList) { writeValue(generator, key.getJavaType(), v); } generator.writeEndArray(); } else { writeValue(generator, key.getJavaType(), val); } } generator.writeEndObject(); } private static void writeValue(JsonGenerator generator, JavaType type, Object val) throws IOException { switch (type) { case BOOLEAN: generator.writeBoolean((Boolean) val); break; case BYTE_STRING: /* TODO: Improve to use read-only byte buffer */ generator.writeBinary(((ByteString) val).toByteArray()); break; case DOUBLE: generator.writeNumber((Double) val); break; case ENUM: generator.writeNumber(((EnumValueDescriptor) val).getNumber()); break; case FLOAT: generator.writeNumber((Float) val); break; case INT: generator.writeNumber((Integer) val); break; case LONG: generator.writeNumber((Long) val); break; case MESSAGE: writeMessage(generator, (Message) val); break; case STRING: generator.writeString((String) val); break; default: throw new IllegalStateException("Unsupported java type: " + type); } } private static class JsonFormatDelimitedEncoder implements Closeable { private final JsonGenerator generator; public JsonFormatDelimitedEncoder(OutputStream os) throws IOException { if (os == null) { throw new NullPointerException(); } this.generator = FACTORY.createJsonGenerator(os, JsonEncoding.UTF8); this.generator.writeStartArray(); } public JsonFormatDelimitedEncoder(Writer writer) throws IOException { if (writer == null) { throw new NullPointerException(); } this.generator = FACTORY.createJsonGenerator(writer); this.generator.writeStartArray(); } public void writeDelimitedTo(Message message) throws IOException { writeMessage(this.generator, message); } @Override public void close() throws IOException { if (this.generator != null) { this.generator.writeEndArray(); this.generator.close(); } } } private static class JsonFormatDelimitedDecoder implements Closeable { private final JsonParser parser; private final ExtensionRegistry registry; public JsonFormatDelimitedDecoder(InputStream is, ExtensionRegistry registry) throws IOException { if (is == null) { throw new NullPointerException(); } this.parser = FACTORY.createJsonParser(is); this.registry = registry; JsonToken tok = this.parser.nextToken(); if (tok != JsonToken.START_ARRAY) { throw new IOException("Expected START_ARRAY"); } } public JsonFormatDelimitedDecoder(Reader reader, ExtensionRegistry registry) throws IOException { if (reader == null) { throw new NullPointerException(); } this.parser = FACTORY.createJsonParser(reader); this.registry = registry; JsonToken tok = this.parser.nextToken(); if (tok != JsonToken.START_ARRAY) { throw new IOException("Expected START_ARRAY"); } } @SuppressWarnings("unchecked") public <T extends Message> T mergeDelimitedFrom(Message message) throws IOException { JsonToken tok = this.parser.nextToken(); if (tok == null || tok == JsonToken.END_ARRAY) { return null; } return (T) readMessage(this.parser, message.newBuilderForType(), this.registry); } @Override public void close() throws IOException { if (this.parser != null) { this.parser.close(); } } } }